Skip to content

feat: delegate issue triage to subject agents - #584

Open
neubig wants to merge 6 commits into
factory/agent-conversation-dispatchfrom
factory/triage-subject-turns
Open

neubig wants to merge 6 commits into
factory/agent-conversation-dispatchfrom
factory/triage-subject-turns

Conversation

@neubig

@neubig neubig commented Sep 14, 2026

Copy link
Copy Markdown
Member

Why

Issue polling and deduplication are deterministic scanner work. Only interpreting an issue, refining its acceptance criteria, and assigning priority require an agent. Keeping those boundaries separate lets one scan start several independent agents and lets Agent Server choose local or Docker execution.

Summary

Keep github-issue-triage as a scheduled scanner. For each changed eligible issue it:

  • computes a delivery digest from the issue and human discussion;
  • delegates through the shared KV-backed conversation helper;
  • uses immutable repository ID plus issue number as the stable subject;
  • asks the agent to publish readable triage, priority, and ready-for-dev directly on GitHub.

The scanner contains no runtime provisioning, profile loading, or custom conversation API.

Issue Number

Closes #583.

How to Test

uv run --group test pytest -q tests/test_github_triage_delivery.py tests/test_automation_setup.py

Result: 126 passed, 18 skipped.

Dependencies

Review and merge #592 first. Running from Canvas with a selected profile additionally requires OpenHands/automation#479. Profile-selected saved secrets require OpenHands/software-agent-sdk#5017; Docker-backed conversations require OpenHands/software-agent-sdk#3403.

Live evidence

From a fresh UI-configured Canvas, the automation independently triaged Airbnb issues #81#86. Issue #83's triage shows the readable rationale, explicit acceptance criteria, priority, and ready-for-dev result produced by this head.

@github-actions github-actions Bot added the type: feat A new feature label Sep 14, 2026
@neubig
neubig marked this pull request as ready for review September 14, 2026 23:25
@neubig

neubig commented Sep 14, 2026

Copy link
Copy Markdown
Member Author

@all-hands-bot live Agent Canvas evidence is now included. Please review this current head and submit an explicit APPROVE review if there are no blocking findings.

@all-hands-bot

Copy link
Copy Markdown
Contributor

👋 This PR needs a couple of things fixed before OpenHands can review it:

  • the PR description's HUMAN: section needs at least 20 characters describing what you tested, not just the template placeholder

Push an update once this is addressed and this check re-runs automatically.

This is an automated check - no AI was used to generate this comment.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat: delegate issue triage to subject agents

I reviewed the current head (factory/triage-subject-turns) against main, ran the full test suite, and checked catalog/bundle consistency.

What the PR does

The github-issue-triage extension is refactored from an SDK-coupled automation that ran its own conversation runtime into a lightweight scanner that delegates triage work to Automation's subject-turn API. The worker no longer imports openhands.sdk (RemoteConversation, RemoteWorkspace, register_default_tools) or manages AGENT_SERVER_URL / SESSION_API_KEY / AUTOMATION_CONVERSATION_ID. Instead, submit_subject_turn() posts each changed issue to AUTOMATION_SUBJECT_TURN_URL with a Bearer $AUTOMATION_RUN_TOKEN credential, keyed by GitHub's immutable repository ID + issue number, and idempotent via a content digest.

Verification performed

  • Full test suite: 823 passed, 24 skipped (includes the 3 new triage delivery tests, all catalog/sync/schema tests).
  • Ruff: clean on worker.py and test_github_triage_delivery.py.
  • Generated catalog: npm run build:automations and npm run build:skills produce no diff — bundle-index.js and skills/index.js are in sync with source files.
  • Bundle-index ↔ source: confirmed worker.py and github_client.py in bundle-index.js match the source files byte-for-byte.

Code quality observations (positive)

  • Clean separation of concerns: the scanner is a plain host command; only the delegated agent receives a conversation runtime. SDK imports, workspace lifecycle, and session management are fully removed.
  • Idempotency: subject_key uses {repository_id}:issue:{number} (immutable repo ID, survives renames) and idempotency_key uses the content digest, so re-runs are safe.
  • Defensive improvements: issue.get("labels", []) instead of issue["labels"], sort_keys=True in the digest serialization for deterministic ordering.
  • Prompt security: the delegated prompt passes only the env-var name (token_name), never the value, instructs the agent never to print it, and scopes mutations to a single issue in a single repository.
  • Tests are real, not mocks of mocks: the tests exercise the actual worker.py entrypoint via the github_automation_helpers fixture (which reads the manifest and loads the real bundle), mocking only the submit_subject_turn and urlopen boundaries.

Non-blocking finding

SKILL.md throughput description is stale. The last paragraph still reads:

Each scheduled run triages at most one changed issue per repository.

The old code used break after the first changed issue (one per run). The new code uses continue to skip already-triaged issues and submits all changed eligible issues per repository per run. The PR description itself says "For every changed eligible issue, it submits an idempotent subject turn," which matches the code. The SKILL.md text should be updated to reflect this (e.g., "Each scheduled run triages all changed eligible issues per repository"). This is documentation-only and does not block merge.

Verdict

No blocking findings. The architectural change is sound, the code is clean and well-tested, and the generated assets are consistent. Approving.

Generated by OpenHands AI on behalf of the user.

@all-hands-bot

Copy link
Copy Markdown
Contributor

@neubig I reviewed the current head and submitted an explicit APPROVE review (#584 (review)).

Verification performed:

  • Full test suite: 823 passed, 24 skipped (including the 3 new triage delivery tests).
  • Ruff: clean on worker.py and test_github_triage_delivery.py.
  • Generated catalog: npm run build:automations and npm run build:skills produce no diff — bundle-index.js and skills/index.js are in sync with source files.
  • Bundle-index ↔ source: confirmed worker.py and github_client.py match byte-for-byte.

Non-blocking finding: SKILL.md's last paragraph still says "Each scheduled run triages at most one changed issue per repository," but the new code uses continue (not break) and submits all changed eligible issues per run. The PR description already reflects the correct behavior. This is documentation-only and does not block merge.

Generated by OpenHands AI on behalf of the user.

@neubig
neubig force-pushed the factory/triage-subject-turns branch from c0634a5 to 4c0c313 Compare September 15, 2026 01:08
@neubig

neubig commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

@all-hands-bot Please re-review the current rebased head and explicitly approve it if there are no blocking findings.

@all-hands-bot
all-hands-bot removed their request for review September 15, 2026 01:08

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of rebased head (4c0c313)

I re-reviewed the current rebased head (factory/triage-subject-turns @ 4c0c313) against main, re-ran the full suite, and re-checked catalog/bundle consistency.

Verification performed

  • Full test suite: 905 passed, 24 skipped (includes the 3 triage delivery tests).
  • Ruff: clean on worker.py and test_github_triage_delivery.py.
  • Generated catalog: npm run build:automations and npm run build:skills produce no diff — bundle-index.js and skills/index.js are in sync with source.
  • Bundle-index ↔ source: worker.py and github_client.py in bundle-index.js match the source files byte-for-byte.

Re-review observations

The rebased head is content-identical to the head I approved previously — the rebase replayed cleanly onto the latest main with no conflicts or changes. The architectural refactor (scanner delegates to Automation's subject-turn API; no SDK imports, no workspace/conversation lifecycle in the worker) remains sound:

  • submit_subject_turn() posts {source, subject_key, turn, idempotency_key} to AUTOMATION_SUBJECT_TURN_URL with a Bearer $AUTOMATION_RUN_TOKEN — no conversation/session/workspace machinery leaked into the scanner.
  • Idempotency keys are {repository_id}:issue:{number} (immutable repo ID, survives renames) + content digest with sort_keys=True for deterministic ordering.
  • The prompt passes only the env-var name (token_name), never the value, and scopes mutations to a single issue in a single repository.
  • Tests exercise the real worker.py entrypoint via the github_automation_helpers fixture, mocking only the submit_subject_turn / urlopen boundaries.

Non-blocking finding (unchanged)

SKILL.md's last paragraph (and the inlined skills/index.js content) still says "Each scheduled run triages at most one changed issue per repository," but the code uses continue (not break) and submits all changed eligible issues per run. The PR description already states the correct behavior. Documentation-only; does not block merge.

Verdict

No blocking findings. Explicitly approving.

Generated by OpenHands AI on behalf of the user.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of rebased head (4c0c313)

I re-reviewed the current rebased head (factory/triage-subject-turns @ 4c0c313) against main, re-ran the full suite, and re-checked catalog/bundle consistency.

Verification performed

  • Full test suite: 905 passed, 24 skipped (includes the 3 triage delivery tests).
  • Ruff: clean on worker.py and test_github_triage_delivery.py.
  • Generated catalog: npm run build:automations and npm run build:skills produce no diff — bundle-index.js and skills/index.js are in sync with source.
  • Bundle-index ↔ source: worker.py and github_client.py in bundle-index.js match the source files byte-for-byte.

Re-review observations

The rebased head is content-identical to the head I approved previously — the rebase replayed cleanly onto the latest main with no conflicts or changes. The architectural refactor (scanner delegates to Automation's subject-turn API; no SDK imports, no workspace/conversation lifecycle in the worker) remains sound:

  • submit_subject_turn() posts {source, subject_key, turn, idempotency_key} to AUTOMATION_SUBJECT_TURN_URL with a Bearer $AUTOMATION_RUN_TOKEN — no conversation/session/workspace machinery leaked into the scanner.
  • Idempotency keys are {repository_id}:issue:{number} (immutable repo ID, survives renames) + content digest with sort_keys=True for deterministic ordering.
  • The prompt passes only the env-var name (token_name), never the value, and scopes mutations to a single issue in a single repository.
  • Tests exercise the real worker.py entrypoint via the github_automation_helpers fixture, mocking only the submit_subject_turn / urlopen boundaries.

Non-blocking finding (unchanged)

SKILL.md's last paragraph (and the inlined skills/index.js content) still says "Each scheduled run triages at most one changed issue per repository," but the code uses continue (not break) and submits all changed eligible issues per run. The PR description already states the correct behavior. Documentation-only; does not block merge.

Verdict

No blocking findings. Explicitly approving.

Generated by OpenHands AI on behalf of the user.

@all-hands-bot

Copy link
Copy Markdown
Contributor

@neubig I re-reviewed the current rebased head (4c0c313) against main and submitted an explicit APPROVE review.

Verification performed:

  • Full test suite: 905 passed, 24 skipped (includes the 3 triage delivery tests).
  • Ruff: clean on worker.py and test_github_triage_delivery.py.
  • Generated catalog: npm run build:automations and npm run build:skills produce no diff — bundle-index.js and skills/index.js are in sync with source.
  • Bundle-index ↔ source: worker.py and github_client.py match byte-for-byte.

The rebased head is content-identical to the head I approved previously — the rebase replayed cleanly onto the latest main with no conflicts or changes. The scanner→subject-turn delegation, idempotency keys (immutable repo ID + sort_keys=True content digest), and prompt security (env-var name only, single-issue scope) all hold up.

Non-blocking finding (unchanged): SKILL.md's last paragraph still says "Each scheduled run triages at most one changed issue per repository," but the code uses continue and submits all changed eligible issues per run. The PR description already reflects the correct behavior. Documentation-only; does not block merge.

No blocking findings. Approved.

Generated by OpenHands AI on behalf of the user.

@neubig
neubig force-pushed the factory/triage-subject-turns branch 2 times, most recently from deec2ae to 05ad737 Compare September 15, 2026 21:40
@neubig

neubig commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

@all-hands-bot Please review the current head after the per-issue failure-isolation fix. If there are no material findings, please submit an approving review rather than only a comment.

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of head 05ad737 — per-issue failure-isolation fix

I reviewed the current head (factory/triage-subject-turns @ 05ad737) after the per-issue failure-isolation commit, re-ran the full suite, and re-checked catalog/bundle consistency.

What changed since the last approval

The per-issue submission logic was extracted from run() into a new _submit(self, repository_id, issue, issues) method, and the run() loop now wraps each call in a try/except:

for issue in sorted(issues, key=lambda item: item["number"]):
    try:
        self._submit(repository_id, issue, issues)
    except Exception as exc:
        print(
            f"Failed to submit {self.repository} issue "
            f"#{issue.get('number', '?')}: {exc}",
            flush=True,
        )

Previously a single submit_subject_turn failure (network error, 5xx from the Automation service, etc.) aborted the entire run, leaving all subsequent issues untriaged. Now each issue is isolated — a failure on one is logged and the remaining issues still get submitted. This mirrors the per-repository isolation already present in run_repositories (github_client.py).

The _submit method is a faithful extraction of the inline code: the early continue for already-triaged issues became an early return, which is equivalent. The idempotency keys, digest, marker, and prompt are unchanged.

Verification performed

  • Full test suite: 906 passed, 24 skipped (includes the new test_triage_continues_after_one_submission_fails test, which verifies that a RuntimeError on issue #1 does not prevent issue #2 from being submitted).
  • Ruff: 1 finding on worker.py (see non-blocking finding below). test_github_triage_delivery.py is clean.
  • Generated catalog: npm run build:automations and npm run build:skills produce no diff — bundle-index.js and skills/index.js are in sync with source.
  • Bundle-index ↔ source: worker.py and github_client.py in bundle-index.js match the source files byte-for-byte.
  • SKILL.md: The previously-noted stale throughput description ("at most one changed issue per repository") is now corrected — it reads "Each scheduled run submits every changed eligible issue. A failure on one issue is reported and does not prevent the remaining issues from being submitted."

Non-blocking finding

Ruff BLE001 regression on worker.py. The new except Exception as exc: at line 120 lacks the # noqa: BLE001 suppression that the codebase's established pattern uses (github_client.py line 196: except Exception as exc: # noqa: BLE001 - one repository must not block others). Consequently ruff check worker.py now reports one error. Ruff is not enforced in CI, so this does not break the build, but it is a lint regression from the previously-clean state. The fix is one line — add # noqa: BLE001 - one issue must not block others to match the convention.

Verdict

No blocking findings. The failure-isolation fix is correct, minimal, well-tested, and the generated assets are consistent. Approving.

Generated by OpenHands AI on behalf of the user.

@all-hands-bot

Copy link
Copy Markdown
Contributor

@neubig I reviewed the current head (05ad737) after the per-issue failure-isolation fix and submitted an explicit APPROVE review.

Verification performed:

  • Full test suite: 906 passed, 24 skipped (includes the new test_triage_continues_after_one_submission_fails test).
  • Ruff: 1 non-blocking finding on worker.py (see below); test_github_triage_delivery.py is clean.
  • Generated catalog: npm run build:automations and npm run build:skills produce no diff — bundle-index.js and skills/index.js are in sync with source.
  • Bundle-index ↔ source: worker.py and github_client.py match byte-for-byte.
  • SKILL.md: the previously-noted stale "at most one changed issue" text is now corrected and documents the failure-isolation behavior.

The fix: The per-issue submission logic was extracted into a _submit method, and the run() loop now wraps each call in a try/except so a single issue's failure is logged and doesn't abort the remaining issues. This mirrors the per-repository isolation already in run_repositories.

Non-blocking finding: worker.py line 120 catches except Exception as exc: without the # noqa: BLE001 suppression that the established pattern uses (github_client.py line 196). Ruff is not enforced in CI so this doesn't break the build, but it's a lint regression from the previously-clean state. One-line fix: add # noqa: BLE001 - one issue must not block others.

No blocking findings. Approved.

Generated by OpenHands AI on behalf of the user.

@neubig
neubig force-pushed the factory/triage-subject-turns branch from d3c3b1c to ce3b6a6 Compare September 16, 2026 03:46
@neubig
neubig changed the base branch from main to factory/agent-conversation-dispatch September 16, 2026 03:46
@neubig
neubig force-pushed the factory/triage-subject-turns branch 2 times, most recently from 9f485e7 to dae808a Compare September 16, 2026 05:41
@neubig
neubig force-pushed the factory/agent-conversation-dispatch branch from c96290d to 1e0a258 Compare September 16, 2026 05:43
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: openhands <openhands@all-hands.dev>
@neubig
neubig force-pushed the factory/triage-subject-turns branch from dae808a to d3bb192 Compare September 16, 2026 05:43
Co-authored-by: openhands <openhands@all-hands.dev>
@neubig
neubig force-pushed the factory/triage-subject-turns branch from d3bb192 to b6cd5b7 Compare September 16, 2026 05:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delegate GitHub issue triage through automation subject turns

3 participants